跳至主要内容

課程:RTK Query 資料管理 第 1 堂:RTK Query 概念建立

30問題與解決方案

在進入 Redux Toolkit Query(下稱 RTK Query)的程式碼實作之前,我們必須先回答一個最根本的問題:為什麼我們需要它?

作為一名有經驗的 React 開發者,你一定寫過無數次的 useEffect 搭配 fetchaxios。這種模式在小型專案中運作良好,但隨著應用程式規模擴大,你會發現非同步資料管理(Asynchronous Data Management)變得異常複雜。這種複雜度並非來自業務邏輯本身,而是來自於「網路請求」與「UI 狀態」之間的各種邊界情況。

本章節將深入解構傳統資料抓取模式的四大結構性痛點,並探討 RTK Query 如何透過全新的心智模型,將你從瑣碎的狀態同步中解放出來。

傳統 useEffect + fetch 的四大痛點

假設你正在開發一個複雜的儀表板,頁面上有多個元件同時依賴於同一個「使用者資訊」API。在不使用任何資料庫管理庫的情況下,你可能會遇到以下問題:

1. 重複請求 (Duplicate Requests)

這是最直觀的效能浪費。當頁面上的「側邊欄(Sidebar)」和「個人主頁(Profile)」兩個元件同時掛載(Mount)時,如果它們都各自在 useEffect 中發送 API 請求,瀏覽器的 Network 面板會出現兩條一模一樣的請求。

這不僅增加了伺服器的負擔,也可能導致不一致的 UI 表現。如果網路稍有延遲,兩個元件取得資料的時間點不同,甚至可能出現資料不完全同步的尷尬情況。在傳統 React 開發中,要解決這個問題,你必須手動將資料提升到全域狀態(如 Redux 或 Context),並撰寫複雜的邏輯來判斷「資料是否正在抓取中,如果是,則不要發送第二次」。

2. 快取缺失與 Loading 閃爍

使用者體驗(UX)中最大的敵人之一就是「不必要的 Loading 狀態」。

想像一個情境:使用者從「文章列表」點進「文章詳情」,然後按瀏覽器的回退鍵回到「文章列表」。在傳統模式下,列表元件會重新掛載,useEffect 會再次執行,使用者會再次看到 0.5 秒的 Loading 旋轉圖。

這就是缺乏**快取機制(Caching)**的後果。資料其實幾秒前才拿過,但因為它只儲存在元件的區域狀態中,隨著元件卸載(Unmount),資料就被銷毀了。為了優化這一點,開發者通常得手動實作一套快取系統,判斷資料的「新鮮度(Freshness)」,這往往會讓專案變得難以維護。

3. Loading 與 Error 狀態的散落

這是一個典型的代碼冗餘問題。在每個需要資料的元件裡,你通常會看到這段熟悉的樣板程式碼:

const [data, setData] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
setIsLoading(true);
fetchUser()
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setIsLoading(false));
}, []);

如果你的專案有 50 個 API 端點,你就得寫 50 次類似的邏輯。更糟糕的是,當多個元件共享同一份資料時,如何同步它們的 isLoading 狀態?當其中一個元件重新整理(Refetch)資料時,其他元件如何得知現在正處於「背景更新中」?

4. 競爭條件 (Race Condition)

這是最隱蔽、也最難除錯的 Bug。讓我們用一個「搜尋框」的場景來分析:

  1. 使用者在搜尋框輸入「A」,發送請求(Request A)。
  2. 使用者緊接著輸入「AB」,發送請求(Request B)。
  3. Request B 因為資料量小,在 100ms 內就回傳了,UI 顯示了「AB」的搜尋結果。
  4. Request A 因為伺服器負載,延遲了 500ms 才回傳。

結果:UI 最終顯示的是「A」的搜尋結果,但搜尋框裡卻顯示著「AB」。這就是競爭條件:後發送的請求不一定後抵達,但若沒妥善處理,舊的資料可能會覆蓋新的資料。

雖然可以透過 AbortController 來取消舊請求,但要在每個元件中正確實作這套機制,對開發者來說是巨大的心智負擔。


RTK Query 的核心價值:自動化管理

RTK Query 並非只是 fetch 的包裝(Wrapper),它是一個完整的資料管理引擎。它將「網路請求」視為一種「快取狀態」,並圍繞著這套快取建立了一系列自動化機制。

自動快取與去重複 (Deduplication)

RTK Query 引入了「請求去重複」的概念。當多個元件同時調用同一個 Hook(例如 useGetUserQuery())時,RTK Query 會檢查內部狀態:

  • 如果請求已在進行中:後續的元件會直接訂閱同一個正在進行的 Promise,不會發送額外的網路請求。
  • 如果快取中已有資料且未過期:RTK Query 會立即回傳快取資料,完全跳過網路請求,達成秒開的體驗。

它是如何識別「相同請求」的?RTK Query 會根據 Endpoint 名稱加上參數生成一個唯一的快取 Key。只要 Key 相同,它就視為同一份資料的需求。

宣告式的資料生命週期

在傳統模式中,我們是「指令式」的:

「當這個元件掛載時,去幫我執行這個 fetch 函式。」

在 RTK Query 中,我們轉向「宣告式」:

「我這個元件需要這份 ID 為 1 的使用者資料。」

你不再需要關心資料什麼時候該抓、什麼時候該更新。RTK Query 會幫你追蹤:

  • 現在有幾個元件在使用這份資料?(訂閱計數)
  • 當所有元件都卸載後,這份快取要保留多久?(預設 60 秒後清除)
  • 當資料被標記為「失效(Stale)」時,是否需要在背景重新獲取?

內建的狀態追蹤與錯誤處理

RTK Query 自動生成的 Hook 會直接提供你所有需要的狀態欄位:dataerrorisLoading(第一次載入)、isFetching(任何時候的抓取,包含背景更新)、isSuccess 等。這些狀態是全域同步的。如果元件 A 觸發了資料重新整理,元件 B 的 isFetching 會自動變成 true,因為它們訂閱的是同一份快取。


心智模型的轉變:從「過程」到「需求」

學習 RTK Query 最重要的不是背誦 API,而是理解心智模型的轉變。

過去:關注「抓取過程」

以前我們的思考邏輯是:

  1. 建立 Loading 狀態。
  2. 觸發非同步動作。
  3. 等待結果。
  4. 手動更新 Store 或區域狀態。
  5. 處理錯誤。

這種模式下,資料被視為一種「暫時的、由過程產生的副作用」。

現在:關注「資料需求」

使用 RTK Query 後,你的思考邏輯變成:

  1. 定義來源:定義一個 API Slice,告訴程式有哪些端點(Endpoints)。
  2. 宣告需求:在元件中使用 Hook 宣告「我需要這份資料」。
  3. 自動同步:RTK Query 負責確保資料出現在畫面上,並處理所有的快取、同步與競爭條件。

你會發現,你寫的程式碼不再有 useEffect,不再有 useState 來存 API 資料。你的元件變得非常乾淨,只專注於「如何根據當前的 dataisLoading 來渲染 UI」。

為什麼這對進階開發者很重要?

作為進階開發者,我們追求的是可預測性一致性

RTK Query 透過強制的結構(API Slice)與自動化處理,消滅了手動管理非同步狀態時產生的無數 Edge Cases。它讓你能夠以極低的代碼量,實作出如「背景同步」、「樂觀更新(Optimistic Updates)」或「自動重新請求」等進階功能。

這不僅僅是減少了幾行程式碼,更是將非同步資料的處理邏輯從 UI 層級徹底抽離,放到了它該去的地方:一個可觀測、可除錯的 Redux 全域狀態管理系統中。

總結與承接

在本章中,我們分析了傳統 React 非同步處理的缺陷,並理解了 RTK Query 如何透過自動快取與去重複機制解決這些痛點。這種從「指令式抓取」到「宣告式需求」的轉變,是現代 React 開發中最顯著的進步之一。

理解了「為什麼」之後,接下來我們需要理解「是什麼」。在下一個部分中,我們將探討 RTK Query 的架構定位:它與你熟悉的 createSlice 有什麼不同?它是如何與 Redux Store 整合的?以及那四大核心組成部分(createApi, baseQuery, endpoints, Hooks)又是如何協作的。

關鍵概念回顧

  • 重複請求與去重複:RTK Query 透過快取 Key 確保相同的請求在同一時間只會發送一次。
  • 競爭條件解決:內建處理請求序號,確保 UI 永遠顯示最後一次請求的正確結果。
  • 訂閱制快取:資料的存在與否取決於是否有元件正在使用(訂閱)它,而非手動維護。
  • 心智模型:開發者只需「宣告需求」,由 RTK Query 負責「管理過程」。